跳到主要内容

Spring基础知识 AOP

什么是 AOP

参考资料 AOP 操作术语 参考资料 Spring AOP-api 参考资料 javaguide AOP

注:如果要直接使用可以看下面注解使用 AOP 那一节,它主要用于做日志输出,例如希望某个方法被调用时打印日志

AOP:Aspect Oriented Programming

面向切面的编程 实际上就是一种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想,它能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。

Spring AOP就是基于动态代理的,如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候Spring AOP会使用Cglib ,这时候Spring AOP会使用 Cglib 生成一个被代理对象的子类来作为代理

主要实现方式:

1、JDK 自带的代理:基于接口的动态代理技术(目标对象必须有接口)

2、cglib 代理:基于父类的动态代理技术(可以无需接口,因为是就是拿目标对象当父类,但是原理不是继承),CGLIB 包的底层是通过使用一个小而快的字节码处理框架 ASM(Java 字节码操控框架),来转换字节码并生成新的类。

AOP 的三个概念

关键就三个东西 “切入点”、“通知”(或者叫增强)、“切面”

通知/增强:增强的逻辑,称为增强,比如扩展日志功能,这个日志功能称为增强。如下:

@Before             前置通知:在方法之前执行
@AfterReturning 后置通知:在方法之后执行
@Around 环绕通知:在方法之前和之后执行​​​​​​​
@AfterThrowing 异常通知:方法出现异常执行
@After 最终通知:无论是否有异常都会执行

切入点(PointCut):所谓切入点是指要对 哪些 连接点进行拦截(就是定义一个规则,这个规则就是切入点)

连接点(JointPoint):就是具体的某个需要被增强的方法

切面(Aspect):切面就是切入点这个规则匹配到的所有方法

具体例子说明

使用前需要在 beans 里加上 aop 的属性依赖

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">

下面就是具体的切入通知的实例

<!-- 这两个就是要切进去的 “通知” -->
<bean id="beforeLog" class="com.alsritter.log.BeforeLog"/>
<bean id="afterLog" class="com.alsritter.log.AfterLog"/>

<!-- 要被切入的对象 -->
<bean id="userService" class="com.alsritter.service.UserServiceImp"/>

<!-- 配置 AOP 织入 -->
<aop:config>
<!-- 配置切入点规则:express:表达式,express(要执行的位置) 最后的这个.*(..) 表示所有方法-->
<!-- 可以看到,满足这个切入点规则的所有方法合称为一个 “切面” -->
<aop:pointcut id="pointcut" expression="execution(* com.alsritter.service.UserServiceImp.*(..))"/>

<!-- 注入通知 -->
<aop:after advice-ref="afterLog" pointcut-ref="pointcut"/>
<aop:before advice-ref="beforeLog" pointcut-ref="pointcut"/>
</aop:config>

两种切入方式

参考资料 aspect 与 advisor 的区别

<aop:aspect>:面向切面编程时,定义切面(切面包括通知和切点)

<aop:advisor>:进行事务管理时,定义通知器(通知器跟切面一样,也包括通知和切点)

上面两种都是用来切入通知的,只是它们的使用方式有点区别

在使用方式上 aspect 无需继承接口,直接定义一个类,指定其中哪个方法切入哪里就可以了,而 advisor 需要继承一个接口(例如 AfterReturningAdvice)才能完成切入

继承接口来编写通知的方式,每次只能编写一个特定的通知(例如继承了 AfterReturningAdvice 就只能写一个后置通知),所以如果不是必须使用到目标对象的方法体或其参数的话一般还是使用 <aop:aspect> 来进行切面编程

但是使用 advisor 也有其优点,继承接口的方式可以直接使用反射来取得被注入对象的方法

具体的使用例子看下面的 一个通知的切面、多个通知的切面

advisor 使用方法

可以直接定义一个通知类,使用这个通知类里面的方法对对象进行切入通知

定义一个通知类

定义一个类用作给切面提供通知

public class DiyPointCut {
public void before(){
System.out.println("========方法执行前=========");
}

public void after(){
System.out.println("========方法执行后=========");
}
}

AOP 的配置

....

<bean id="diy" class="com.alsritter.diy.DiyPointCut"/>
<aop:config>
<!-- 自定义切面,ref 要引用的类-->
<aop:aspect ref="diy">
<!-- 切入点-->
<aop:pointcut id="pointcut" expression="execution(* com.alsritter.service.UserServiceImp.*(..))"/>
<!-- 通知 -->
<aop:after method="after" pointcut-ref="pointcut"/>
<aop:before method="before" pointcut-ref="pointcut"/>
</aop:aspect>
</aop:config>

测试类

public class MyTest {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
UserService userService = (UserService) context.getBean("userService");
userService.add();
}
}

advisor 使用方法

这里使用的是 advisor,它需要让通知继承指定类型的接口,然后才能把这个通知切入到某个对象中

配置通知的接口

afterLog 继承 AfterReturningAdvice 接口

public class AfterLog implements AfterReturningAdvice {
@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
System.out.println("执行了"+method.getName()+"返回结果为:"+returnValue);
}
}

BeforeLog 继承 MethodBeforeAdvice 接口

public class BeforeLog implements MethodBeforeAdvice {

@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println(target.getClass().getSimpleName()+"的"+method.getName()+"被执行了");
}
}

AOP 的配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">

<!-- 先配置好Bean -->
<bean id="beforeLog" class="com.alsritter.log.BeforeLog"/>
<bean id="afterLog" class="com.alsritter.log.AfterLog"/>
<bean id="userService" class="com.alsritter.service.UserServiceImp"/>

<!-- 配置 AOP 织入-->
<aop:config>
<!-- 配置切入点:express:表达式,express(要执行的位置) 最后的这个.*(..) 表示所有方法-->
<aop:pointcut id="pointcut" expression="execution(* com.alsritter.service.UserServiceImp.*(..))"/>

<!-- 注入通知 注意使用的是 advisor -->
<aop:advisor advice-ref="afterLog" pointcut-ref="pointcut"/>
<aop:advisor advice-ref="beforeLog" pointcut-ref="pointcut"/>
</aop:config>

</beans>

这里的AOP切入点语法参考:【execution】

execution( [ 修饰符 ] 返回值类型 包名.类名.方法名( 参数 ))

测试类

调用时就像平常那样用就行了,可见,AOP 是完全不需要动源码

public class MyTest {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
UserService userService = (UserService) context.getBean("userService");
userService.add();
}
}

注解的方式定义

配置环境

只需导入这个 aspectjweaver,SpringAOP 已经集成到 Context 上了

<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.5</version>
</dependency>

然后在 beans 里加上 aop 的属性依赖

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">

开启注解支持

<!-- 指定要扫描的包,这个包下的注解就会生效 -->
<context:component-scan base-package="com.alsritter.aop"/>
<!-- 配置 aop 自动代理 -->
<aop:aspectj-autoproxy/>

使用方法

@Before             前置通知:在方法之前执行
@AfterReturning 后置通知:在方法之后执行
@Around 环绕通知:在方法之前和之后执行​​​​​​​
@AfterThrowing 异常通知:方法出现异常执行
@After 最终通知:无论是否有异常都会执行

在类里定义切面

@Aspect // 标注当前类是一个切面
@Component
public class AnnotationPointCut {

//传入一个切入点
@Before("execution(* com.alsritter.service.UserServiceImp.*(..))")
public void before(){
System.out.println("=========方法执行前========");
}
}

测试类同上

public class MyTest {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
UserService userService = (UserService) context.getBean("userService");
userService.add();
}
}

补充 JDK 动态代理

具体细节看 Java 代理模式那篇笔记,这里只是为了方便快速回忆

创建目标类及目标类的接口

public interface TargetInterface {
void save();
}
public class Target implements TargetInterface{
@Override
public void save() {
System.out.println("save...");
}
}

创建通知类

public class Advice {
public void before() {
System.out.println("前置增强");
}

public void after(){
System.out.println("后置增强");
}
}

测试代理类

public class ProxyTest {
public static void main(String[] args) {
Target target = new Target();
Advice advice = new Advice(); // 创建增强的对象

TargetInterface proxy = (TargetInterface) Proxy.newProxyInstance(
target.getClass().getClassLoader(), // 目标对象类加载器
target.getClass().getInterfaces(),
new InvocationHandler() {
// 调用代理对象的任何方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 前置增强
advice.before();
// 调用目标对象的方法
method.invoke(target, args);
// 后置增强
advice.after();
return null;
}
}
);

// 调用代理对象
proxy.save();
}
}

AspectJ

AspectJ 可以单独使用,也可以整合到其它框架中。它是一种基于 Java 平台的面向切面编程的语言

单独使用 AspectJ 时需要使用专门的编译器 ajc。 java 的编译器是 javac,AspectJ 的编译器是 ajc,aj 是首字母缩写,c 即compiler

AspectJ 和 Spring AOP 区别

参考资料 什么是AOP?Spring AOP和AspectJ的区别是什么?

Spring AOP

1、基于动态代理来实现,默认如果使用接口的,用JDK提供的动态代理实现,如果是方法则使用CGLIB实现

2、Spring AOP 需要依赖 IOC 容器来管理,并且只能作用于 Spring 容器,使用纯 Java 代码实现

3、在性能上,由于 Spring AOP 是基于动态代理来实现的,在容器启动时需要生成代理实例,在方法调用上也会增加栈的深度,使得 Spring AOP 的性能不如 AspectJ 的那么好

AspectJ

AspectJ 属于静态织入,通过修改代码来实现,因为 AspectJ 在实际运行之前就完成了织入,所以说它生成的类是没有额外运行时开销的,有如下几个织入的时机:

​1、编译期织入(Compile-time weaving): 如类 A 使用 AspectJ 添加了一个属性,类 B 引用了它,这个场景就需要编译期的时候就进行织入,否则没法编译类 B。

​2、编译后织入(Post-compile weaving): 也就是已经生成了 .class 文件,或已经打成 jar 包了,这种情况我们需要增强处理的话,就要用到编译后织入。

​3、类加载后织入(Load-time weaving): 指的是在加载类的时候进行织入,要实现这个时期的织入,有几种常见的方法。

  1. 自定义类加载器来干这个,这个应该是最容易想到的办法,在被织入类加载到 JVM 前去对它进行加载,这样就可以在加载的时候定义行为了。
  2. 在 JVM 启动的时候指定 AspectJ 提供的 agent:-javaagent:xxx/xxx/aspectjweaver.jar

AspectJ 可以做 Spring AOP 干不了的事情,它是 AOP 编程的完全解决方案,Spring AOP则致力于解决企业级开发中最普遍的 AOP(方法织入)。而不是成为像 AspectJ 一样的 AOP方案

下表总结了 Spring AOP 和 AspectJ 之间的关键区别:

Spring AOPAspectJ
在纯 Java 中实现使用 Java 编程语言的扩展实现
不需要单独的编译过程除非设置 LTW,否则需要 AspectJ 编译器(ajc)
只能使用运行时织入运行时织入不可用。支持编译时、编译后和加载时织入
功能不强-仅支持方法级编织更强大--可以编织字段、方法、构造函数、静态初始值设定项、最终类/方法等
只能在由 Spring 容器管理的 bean 上实现可以在所有域对象上实现
仅支持方法执行切入点支持所有切入点
代理是由目标对象创建的,并且切面应用在这些代理上在执行应用程序之前(在运行时), 各方面直接在代码中进行织入
比 AspectJ 慢多了更好的性能
易于学习和应用相对于 Spring AOP 来说更复杂

具体用法

TODO: 下次使用到时再补充